import sys, os
import time
import random
import json
import multiprocessing
import requests
from selenium import webdriver
from selenium.webdriver.edge.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import (
    ElementClickInterceptedException,
    NoSuchElementException,
    TimeoutException
)
# from fake_useragent import UserAgent  <- [修改] 删除了 fake_useragent 的导入
from retrying import retry
import logging
import traceback

# 设置日志
logging.basicConfig(
    filename='wjx.log',
    level=logging.DEBUG,
    format='%(asctime)s - %(levelname)s - %(message)s',
    encoding='utf-8'
)

# --- 配置变量 ---
# [修改] 下方的全局路径仅作为代码内的备用值，实际路径应由 gui.py 启动时传入
# The global paths below are for fallback only; actual paths should be passed from gui.py.
DRIVER_PATH = r"G:\msedgedriver.exe"
BROWSER_PATH = r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
PROCESSES_COUNT = 1
RUNS_PER_PROCESS = 1
USE_PROXY = False
API_URL = 'https://service.ipzan.com/core-extract?num=1&no=20230525165982903521&minute=1&format=json&protocol=1&pool=quality&secret=uvg8pco497ldfag'

def get_random_proxy():
    if not API_URL:
        logging.error("错误: 未配置 API_URL。")
        return None
    try:
        response = requests.get(API_URL, timeout=15)
        if response.status_code != 200:
            logging.error(f"错误: 代理API返回状态码 {response.status_code}，响应内容: {response.text[:100]}")
            return None
        data = response.json()
        ip_list = data.get('data', {}).get('list', [])
        if not ip_list:
            logging.warning("警告: 代理API返回的IP列表为空。")
            return None
        proxy_info = random.choice(ip_list)
        proxy_address = f"{proxy_info['ip']}:{proxy_info['port']}"
        return {'http': f"http://{proxy_address}", 'https': f"http://{proxy_address}"}
    except requests.exceptions.Timeout:
        logging.error("错误: 获取代理API超时（超过15秒）。")
        return None
    except requests.exceptions.RequestException as e:
        logging.error(f"错误: 请求代理API时发生网络错误 - {e}")
        return None
    except json.JSONDecodeError:
        logging.error(f"错误: 代理API返回的不是有效的JSON格式。响应内容: {response.text[:100]}")
        return None
    except Exception as e:
        logging.error(f"错误: 获取代理时发生未知错误 - {e}")
        return None

def verify_proxy(proxy_dict):
    if not proxy_dict:
        return False
    logging.info(f"正在验证代理: {proxy_dict['http']} ...")
    test_url = 'https://httpbin.org/ip'
    try:
        response = requests.get(test_url, proxies=proxy_dict, timeout=10)
        response.raise_for_status()
        logging.info(f"代理验证成功！代理IP: {response.json()['origin']}")
        return True
    except requests.exceptions.Timeout:
        logging.error(f"代理验证失败: 连接超时。")
        return False
    except requests.exceptions.RequestException as e:
        logging.error(f"代理验证失败: 网络错误 - {e}")
        return False
    except Exception as e:
        logging.error(f"代理验证失败: 未知错误 - {e}")
        return False

def wait_for_question(driver, num):
    try:
        element = WebDriverWait(driver, 3).until(
            EC.visibility_of_element_located((By.CSS_SELECTOR, f'#div{num}'))
        )
        return element.is_displayed()
    except (TimeoutException, NoSuchElementException):
        return False

def safe_click(driver, element, timeout=3):
    try:
        WebDriverWait(driver, timeout).until_not(
            EC.presence_of_element_located((By.CLASS_NAME, 'layui-layer-shade'))
        )
        driver.execute_script("arguments[0].scrollIntoView({block: 'center', inline: 'nearest'});", element)
        clickable_element = WebDriverWait(driver, timeout).until(
            EC.element_to_be_clickable(element)
        )
        clickable_element.click()
    except ElementClickInterceptedException:
        logging.info("常规点击被拦截，尝试使用JavaScript强制点击...")
        driver.execute_script("arguments[0].click();", element)
    except Exception as e:
        logging.error(f"safe_click 执行时发生未知错误: {e}")

@retry(stop_max_attempt_number=3, wait_fixed=1500)
def safe_submit(driver):
    try:
        submit_btn = driver.find_element(By.CSS_SELECTOR, '#ctlNext')
        safe_click(driver, submit_btn)
    except Exception as e:
        logging.error(f"提交时发生错误，正在重试... ({e})")
        driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
        raise

def single_choice(driver, num, weights, **kwargs):
    questions = driver.find_elements(By.CSS_SELECTOR, f'#div{num} > div.ui-controlgroup')
    for question in questions:
        options = question.find_elements(By.CSS_SELECTOR, '.ui-radio')
        if not options: continue
        if weights == -1 or len(weights) != len(options): weights = [1] * len(options)
        selected_option = random.choices(options, weights=weights)[0]
        safe_click(driver, selected_option)
        time.sleep(random.uniform(0.05, 0.15))

def multiple_choice(driver, num, weights, min_choices=-1, max_choices=-1, **kwargs):
    questions = driver.find_elements(By.CSS_SELECTOR, f'#div{num} > div.ui-controlgroup')
    for question in questions:
        options = question.find_elements(By.CSS_SELECTOR, '.ui-checkbox')
        if not options: continue
        n = len(options)
        if weights == -1 or len(weights) != n:
            probs = [random.random() for _ in range(n)]
        else:
            max_weight = max((w for w in weights if w != -1), default=1)
            probs = [(w / max_weight if max_weight > 0 else 0) if w != -1 else random.random() for w in weights]
        selected_indices = {i for i, p in enumerate(probs) if random.random() < p}
        if not selected_indices: selected_indices.add(random.randint(0, n - 1))
        if min_choices != -1 and len(selected_indices) < min_choices:
            remaining = [i for i in range(n) if i not in selected_indices]
            needed = min_choices - len(selected_indices)
            selected_indices.update(random.sample(remaining, min(needed, len(remaining))))
        if max_choices != -1 and len(selected_indices) > max_choices:
            selected_indices = set(random.sample(list(selected_indices), max_choices))
        for idx in selected_indices:
            try:
                if not options[idx].is_selected():
                    safe_click(driver, options[idx])
                    time.sleep(random.uniform(0.1, 0.2))
            except Exception as e:
                logging.error(f"点击多选选项时出错: {e}")

def fill_in_the_blank(driver, num, answers, weights, **kwargs):
    selected_answer = random.choices(answers, weights=weights if weights != -1 else None)[0]
    driver.find_element(By.CSS_SELECTOR, f'#q{num}').send_keys(selected_answer)
    time.sleep(random.uniform(0.05, 0.15))

def matrix_question(driver, num, personality_type, **kwargs):
    rows = driver.find_elements(By.XPATH, f'//*[@id="div{num}"]/div[2]/table/tbody/tr')
    if not rows: return
    dominant_zone, last_selected, random_chance = None, None, 0.1
    for row in rows:
        options = row.find_elements(By.TAG_NAME, 'a')
        n_options = len(options)
        if not options: continue
        if dominant_zone is None:
            zones = {'high_score': range(n_options // 2, n_options),
                     'mid_score': range(n_options // 3, n_options * 2 // 3), 'low_score': range(0, n_options // 2)}
            chances = {'high_score': 0.02, 'mid_score': 0.05, 'low_score': 0.02, 'chaotic': 0.2}
            dominant_zone = random.choice(zones.get(personality_type, range(n_options)))
            random_chance = chances.get(personality_type, 0.2)
        if random.random() < random_chance or last_selected is None:
            weights = [0.1] * n_options
            weights[dominant_zone] = 0.7
            if dominant_zone > 0: weights[dominant_zone - 1] = 0.2
            if dominant_zone < n_options - 1: weights[dominant_zone + 1] = 0.2
            selected_index = random.choices(range(n_options), weights=weights)[0]
        else:
            candidate_indices = {max(0, last_selected - 1), last_selected, min(n_options - 1, last_selected + 1)}
            selected_index = random.choice(list(candidate_indices))
        safe_click(driver, options[selected_index])
        last_selected = selected_index
        time.sleep(random.uniform(0.1, 0.25))

def dimension_question(driver, nums, personality_type, **kwargs):
    logging.info(f"开始处理维度题目: {nums}，人格: {personality_type}")
    dominant_zone, last_selected_index, random_chance = None, None, 0.1
    for num in nums:
        if not wait_for_question(driver, num): continue
        options = driver.find_elements(By.CSS_SELECTOR, f'#div{num} .ui-radio')
        if not options: continue
        n_options = len(options)
        if dominant_zone is None:
            zones = {'high_score': range(n_options // 2, n_options),
                     'mid_score': range(n_options // 3, n_options * 2 // 3), 'low_score': range(0, n_options // 2)}
            chances = {'high_score': 0.02, 'mid_score': 0.05, 'low_score': 0.02, 'chaotic': 0.2}
            dominant_zone = random.choice(zones.get(personality_type, range(n_options)))
            random_chance = chances.get(personality_type, 0.2)
        if random.random() < random_chance or last_selected_index is None:
            weights = [0.1] * n_options
            weights[dominant_zone] = 0.7
            if dominant_zone > 0: weights[dominant_zone - 1] = 0.2
            if dominant_zone < n_options - 1: weights[dominant_zone + 1] = 0.2
            selected_index = random.choices(range(n_options), weights=weights)[0]
        else:
            candidate_indices = {max(0, last_selected_index - 1), last_selected_index,
                                 min(n_options - 1, last_selected_index + 1)}
            selected_index = random.choice(list(candidate_indices))
        try:
            safe_click(driver, options[selected_index])
            last_selected_index = selected_index
            time.sleep(random.uniform(0.1, 0.25))
        except Exception as e:
            logging.error(f"点击维度题目 {num} 选项时出错: {e}")

def scale_question(driver, num, weights, **kwargs):
    options = driver.find_elements(By.XPATH, f'//*[@id="div{num}"]/div[2]/div/ul/li')
    if not options: return
    if weights == -1 or len(weights) != len(options): weights = [1] * len(options)
    selected_option = random.choices(options, weights=weights)[0]
    safe_click(driver, selected_option)
    time.sleep(random.uniform(0.05, 0.15))

def dropdown_question(driver, num, weights, **kwargs):
    dropdown_container = driver.find_element(By.XPATH, f'//*[@id="select2-q{num}-container"]')
    safe_click(driver, dropdown_container)
    options = WebDriverWait(driver, 5).until(
        EC.presence_of_all_elements_located((By.CSS_SELECTOR, '.select2-results__option')))
    if not options: return
    if weights == -1 or len(weights) != len(options): weights = [1] * len(options)
    random_option = random.choices(options, weights=weights)[0]
    safe_click(driver, random_option)
    time.sleep(random.uniform(0.05, 0.15))

def handle_verification(driver):
    try:
        confirm_button = WebDriverWait(driver, 0.5).until(
            EC.element_to_be_clickable((By.CSS_SELECTOR, '.layui-layer-btn a.layui-layer-btn0')))
        safe_click(driver, confirm_button)
        logging.info("已关闭提交确认弹窗。")
    except TimeoutException:
        pass
    try:
        verify_button = WebDriverWait(driver, 0.5).until(EC.element_to_be_clickable((By.ID, 'SM_BTN_1')))
        safe_click(driver, verify_button)
        logging.info("已点击智能验证按钮。")
        time.sleep(1.5)
    except TimeoutException:
        pass
    try:
        slider_handle = driver.find_element(By.ID, 'nc_1_n1z')
        track = driver.find_element(By.ID, 'nc_1__scale_text')
        ActionChains(driver).drag_and_drop_by_offset(slider_handle, track.size['width'], 0).perform()
        logging.info("已尝试拖动滑块。")
        time.sleep(1)
    except NoSuchElementException:
        pass
    except Exception as e:
        logging.error(f"拖动滑块时发生错误: {e}")

@retry(stop_max_attempt_number=2, wait_fixed=2000)
# [重要修改] 新增 driver_path 参数，用于接收从 GUI 传来的驱动路径
def run_survey_task(process_id, times, survey_config, driver_path, browser_path=None, use_proxy_flag=False):
    logging.info(f"进程 {process_id} 开始运行任务")
    proxy_to_use = None
    if use_proxy_flag:
        logging.info(f"进程 {process_id} 已请求使用代理，正在获取和验证...")
        proxy_candidate = get_random_proxy()
        if proxy_candidate and verify_proxy(proxy_candidate):
            proxy_to_use = proxy_candidate
        else:
            logging.error(f"进程 {process_id} 代理无效，本次任务将被中止。")
            return

    function_mapping = {
        "single_choice": single_choice, "two_choice": single_choice,
        "xialakuang": dropdown_question, "multiple_choice": multiple_choice,
        "fill_in_the_blank": fill_in_the_blank, "juzhen": matrix_question,
        "liangbiao": scale_question, "dimension": dimension_question
    }
    url = survey_config['url']
    actions = survey_config['actions']

    options = webdriver.EdgeOptions()
    options.add_argument("--disable-blink-features=AutomationControlled")
    options.add_experimental_option('excludeSwitches', ['enable-logging'])
    options.add_experimental_option("prefs", {"profile.managed_default_content_settings.images": 2})

    if browser_path:
        options.binary_location = browser_path

    if proxy_to_use:
        options.add_argument(f"--proxy-server={proxy_to_use['http']}")
        logging.info(f"进程 {process_id} 已成功配置并验证代理: {proxy_to_use['http']}")

    # [重要修改] 使用从主函数传递过来的 driver_path，而不是全局变量
    service = Service(driver_path)
    driver = None

    try:
        # [修改] 移除了 fake_useragent，改用固定的 User-Agent 字符串
        options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0")
        driver = webdriver.Edge(service=service, options=options)
        logging.info(f'进程 {process_id} 已启动浏览器。')

        for i in range(times):
            logging.info(f'进程 {process_id} 开始第 {i + 1}/{times} 次提交...')
            driver.get(url)

            action_index = 0
            current_page = 1

            while action_index < len(actions):
                logging.info(f"正在处理第 {current_page} 页的题目...")
                action_performed_on_page = False

                temp_action_index = action_index
                while temp_action_index < len(actions):
                    action = actions[temp_action_index]
                    q_num = action.get("params", {}).get("nums", [action.get("params", {}).get("num", 0)])[0]

                    try:
                        question_div = driver.find_element(By.CSS_SELECTOR, f'#div{q_num}')
                        if question_div.is_displayed():
                            action_type = action.get("type")
                            params = action.get("params", {})
                            if action_type in function_mapping:
                                function_mapping[action_type](driver, **params)
                            else:
                                logging.warning(f"警告: 未知操作类型 '{action_type}'")
                            action_index += 1
                            action_performed_on_page = True
                    except NoSuchElementException:
                        pass
                    temp_action_index += 1

                if action_index >= len(actions):
                    logging.info("所有配置的题目均已完成。")
                    break

                if not action_performed_on_page and action_index < len(actions):
                    logging.warning(f"警告: 在第 {current_page} 页没有找到任何预期的题目，但仍有任务剩余。")

                try:
                    next_button = WebDriverWait(driver, 3).until(EC.element_to_be_clickable((By.ID, 'divNext')))
                    safe_click(driver, next_button)
                    current_page += 1
                    logging.info(f"已翻至第 {current_page} 页")
                    time.sleep(random.uniform(0.5, 1))
                except TimeoutException:
                    logging.info(f"未找到“下一页”按钮，认定问卷结束。")
                    break

            logging.info("所有页面处理完毕，准备提交。")
            safe_submit(driver)
            handle_verification(driver)
            time.sleep(2)
            logging.info(f'进程 {process_id} 已成功提交第 {i + 1}/{times} 次问卷')

    except Exception as e:
        logging.error(f'进程 {process_id} 出现严重错误: {e}\n{traceback.format_exc()}')
        raise

    finally:
        if driver:
            driver.quit()
        logging.info(f'进程 {process_id} 已结束')

def run_main(app_config):
    logging.info(f"wjx.py run_main 启动，进程: {multiprocessing.current_process().name}")
    if multiprocessing.current_process().name != 'MainProcess':
        logging.info("在子进程中运行 wjx.py，仅执行任务逻辑")

    survey_file = app_config.get('SURVEY_CONFIG_FILE')
    process_count = app_config.get('PROCESSES_COUNT')
    runs_per_process = app_config.get('RUNS_PER_PROCESS')
    driver_path = app_config.get('DRIVER_PATH')
    browser_path = app_config.get('BROWSER_PATH')
    use_proxy = app_config.get('USE_PROXY', False)
    api_url = app_config.get('API_URL')

    if not all([survey_file, process_count, runs_per_process, driver_path]):
        logging.error("错误: GUI未能提供完整的配置信息。")
        return

    try:
        with open(survey_file, 'r', encoding='utf-8') as f:
            survey_config_data = json.load(f)
        logging.info(f"成功加载问卷配置: {survey_file}")
    except FileNotFoundError:
        logging.error(f"错误：找不到问卷配置文件 '{survey_file}'。")
        return
    except json.JSONDecodeError as e:
        logging.error(f"错误：问卷配置文件 '{survey_file}' 格式不正确: {e}")
        return
    except Exception as e:
        logging.error(f"加载问卷配置时发生未知错误: {e}\n{traceback.format_exc()}")
        return

    if process_count > os.cpu_count():
        logging.warning(f"警告: 进程数 ({process_count}) > CPU核心数 ({os.cpu_count()})，可能影响性能。")
    logging.info(f"准备启动 {process_count} 个进程，每个进程运行 {runs_per_process} 次...")

    processes = [
        multiprocessing.Process(
            target=run_survey_task,
            # [重要修改] 将从 GUI 获取的 driver_path 和 browser_path 传递给子进程任务
            args=(i + 1, runs_per_process, survey_config_data, driver_path, browser_path, use_proxy)
        )
        for i in range(process_count)
    ]

    for p in processes:
        p.start()
        time.sleep(random.uniform(1, 4))
    for p in processes:
        p.join()
    logging.info("所有任务已完成！")

if __name__ == '__main__':
    logging.info(f"wjx.py 主程序启动，进程: {multiprocessing.current_process().name}")
    if multiprocessing.current_process().name != 'MainProcess':
        logging.info("在子进程中，跳过 wjx.py 主逻辑")
    else:
        logging.error("错误: wjx.py 不应直接作为主程序运行")
        sys.exit(1)